<?php
/**
 * Parses unified or context diffs output from eg. the diff utility.
 *
 * Example:
 * <code>
 * $patch = file_get_contents('example.patch');
 * $diff = &new Text_Diff('string', array($patch));
 * $renderer = &new Text_Diff_Renderer_inline();
 * echo $renderer->render($diff);
 * </code>
 *
 * @author    �rjan Persson <o@42mm.org>
 * @copyright Copyright 2005 �rjan Persson
 * @package   Text_Diff
 * @since     0.2.0
 * @access    private
 */
class Text_Diff_Engine_string {

		/**
 		* Parses a unified or context diff.
 		*
 		* First param contains the whole diff and the second can be used to force 
 		* a specific diff type. If the second parameter is 'autodetect', the 
 		* diff will be examined to find out which type of diff this is.
 		* 
 		* @param string $diff  The diff content.
 		* @param string $mode  The diff mode of the content in $diff. One of
 		*                      'context', 'unified', or 'autodetect'.
 		*
 		* @return array  List of all diff operations.
 		*/
		function diff($diff, $mode = 'autodetect')
		{
				if ($mode != 'autodetect' && $mode != 'context' && $mode != 'unified') {
						die_friendly('Text_Diff', '<p>Type of diff is unsupported</p>');
				}

				if ($mode == 'autodetect') {
						$context = strpos($diff, '***');
						$unified = strpos($diff, '---');
						if ($context === $unified) {
								die_friendly('Text_Diff', '<p>Type of diff could not be detected</p>');
						} elseif ($context === false || $context === false) {
								$mode = $context !== false ? 'context' : 'unified';
						} else {
								$mode = $context < $unified ? 'context' : 'unified';
						}
				}

				// split by new line and remove the diff header
				$diff = explode("\n", $diff);
				array_shift($diff);
				array_shift($diff);

				if ($mode == 'context') {
						return $this->parseContextDiff($diff);
				} else {
						return $this->parseUnifiedDiff($diff);
				}
		}

		/**
 		* Parses an array containing the unified diff.
 		*
 		* @param array $diff  Array of lines.
 		*
 		* @return array  List of all diff operations.
 		*/
		function parseUnifiedDiff($diff)
		{
				$edits = array();
				$end = count($diff) - 1;
				for ($i = 0; $i < $end;) {
						$diff1 = array();
						switch (substr($diff[$i], 0, 1)) {
						case ' ':
								do {
										$diff1[] = substr($diff[$i], 1);
								} while (++$i < $end && substr($diff[$i], 0, 1) == ' ');
								$edits[] = &new Text_Diff_Op_copy($diff1);
								break;
						case '+':
								// get all new lines
								do {
										$diff1[] = substr($diff[$i], 1);
								} while (++$i < $end && substr($diff[$i], 0, 1) == '+');
								$edits[] = &new Text_Diff_Op_add($diff1);
								break;
						case '-':
								// get changed or removed lines
								$diff2 = array();
								do {
										$diff1[] = substr($diff[$i], 1);
								} while (++$i < $end && substr($diff[$i], 0, 1) == '-');
								while ($i < $end && substr($diff[$i], 0, 1) == '+') {
										$diff2[] = substr($diff[$i++], 1);
								}
								if (count($diff2) == 0) {
										$edits[] = &new Text_Diff_Op_delete($diff1);
								} else {
										$edits[] = &new Text_Diff_Op_change($diff1, $diff2);
								}
								break;
						default:
								$i++;
								break;
						}
				}
				return $edits;
		}

		/**
 		* Parses an array containing the context diff.
 		*
 		* @param array $diff  Array of lines.
 		*
 		* @return array  List of all diff operations.
 		*/
		function parseContextDiff(&$diff)
		{
				$edits = array();
				$i = $max_i = $j = $max_j = 0;
				$end = count($diff) - 1;
				while ($i < $end && $j < $end) {
						while ($i >= $max_i && $j >= $max_j) {
								// find the boundaries of the diff output of the two files
								for ($i = $j;
 										$i < $end && substr($diff[$i], 0, 3) == '***';
 										$i++);
								for ($max_i = $i;
 										$max_i < $end && substr($diff[$max_i], 0, 3) != '---';
 										$max_i++);
								for ($j = $max_i;
 										$j < $end && substr($diff[$j], 0, 3) == '---';
 										$j++);
								for ($max_j = $j;
 										$max_j < $end && substr($diff[$max_j], 0, 3) != '***';
 										$max_j++);
						}

						// find what hasn't been changed
						$array = array();
						while ($i < $max_i &&
 									$j < $max_j &&
 									strcmp($diff[$i], $diff[$j]) == 0) {
								$array[] = substr($diff[$i], 2);
								$i++;
								$j++;
						}
						while ($i < $max_i && ($max_j-$j) <= 1) {
								if ($diff[$i] != '' && substr($diff[$i], 0, 1) != ' ') {
										break;
								}
								$array[] = substr($diff[$i++], 2);
						}
						while ($j < $max_j && ($max_i-$i) <= 1) {
								if ($diff[$j] != '' && substr($diff[$j], 0, 1) != ' ') {
										break;
								}
								$array[] = substr($diff[$j++], 2);
						}
						if (count($array) > 0) {
								$edits[] = &new Text_Diff_Op_copy($array);
						}

						if ($i < $max_i) {
								$diff1 = array();
								switch (substr($diff[$i], 0, 1)) {
								case '!':
										$diff2 = array();
										do {
												$diff1[] = substr($diff[$i], 2);
												if ($j < $max_j && substr($diff[$j], 0, 1) == '!') {
														$diff2[] = substr($diff[$j++], 2);
												}
										} while (++$i < $max_i && substr($diff[$i], 0, 1) == '!');
										$edits[] = &new Text_Diff_Op_change($diff1, $diff2);
										break;
								case '+':
										do {
												$diff1[] = substr($diff[$i], 2);
										} while (++$i < $max_i && substr($diff[$i], 0, 1) == '+');
										$edits[] = &new Text_Diff_Op_add($diff1);
										break;
								case '-':
										do {
												$diff1[] = substr($diff[$i], 2);
										} while (++$i < $max_i && substr($diff[$i], 0, 1) == '-');
										$edits[] = &new Text_Diff_Op_delete($diff1);
										break;
								}
						}

						if ($j < $max_j) {
								$diff2 = array();
								switch (substr($diff[$j], 0, 1)) {
								case '+':
										do {
												$diff2[] = substr($diff[$j++], 2);
										} while ($j < $max_j && substr($diff[$j], 0, 1) == '+');
										$edits[] = &new Text_Diff_Op_add($diff2);
										break;
								case '-':
										do {
												$diff2[] = substr($diff[$j++], 2);
										} while ($j < $max_j && substr($diff[$j], 0, 1) == '-');
										$edits[] = &new Text_Diff_Op_delete($diff2);
										break;
								}
						}
				}
				return $edits;
		}
}
